#!/usr/bin/python3
import time
import sys
import socket
import io
import signal, os
import subprocess
import threading
import time
import queue
import ctypes
import gi
import os
import sys
import subprocess
import threading
import logging
import sdl2
import sdl2.ext
import sdl2.sdlimage

gi.require_version("Gtk", "3.0")
from gi.repository import GLib,Gtk, Gdk, Gio

# scrpy-client
import logging
import os
import socket
import struct
import subprocess
import sys
from queue import Queue
from time import sleep
import struct
from time import sleep
# import av


title="安卓桌面(ADB虚拟屏幕)"
SCREEN_WIDTH  = 1008 #768
SCREEN_HEIGHT = 1600 #1779 #1367
# SCREEN_WIDTH  = 768
# SCREEN_HEIGHT = 1367
adbport=1111
adbconn="127.0.0.1:"+str(adbport)
app=None
OSVER=1
QUEUE_SIZE=1024
nFrameCache=0
secStart=0
bytesImg=None
pySDLImg=None
exit_request=False
SIG_SCREEN_DIRTY=signal.SIGRTMIN + 8
lkImage = threading.Lock()
eNotify = threading.Event()
qNotify = queue.Queue(QUEUE_SIZE)
process_control_server = None
scrcpy_client = None
NOTIFY_PIPE=os.getenv('NOTIFY_PIPE')

MSGID_UPDATEIMG=1
MSGID_SCR_DIRTY=2
MSGID_SCSTARTED=3

# 不同的组合，可以组成4种不同的运行模式
CONV_IMG_IN_GUI_THREAD=True # 为False时，易出现 Segmentation fault 的错误
DIRTYNOTIFY_VIA_SIGNAL=True

# 默认启动的应用
DEF_PKG="com.zzvm";                 DEF_ACT=".VirtActivity"
DEF_PKG="com.ss.android.ugc.aweme"; DEF_ACT=".splash.SplashActivity"
DEF_PKG="com.tencent.mm";           DEF_ACT=".ui.LauncherUI"

# 日志初始化
logger = logging.getLogger(__name__)
logging.basicConfig(
    stream=sys.stdout,
    level=logging.INFO,
    format='%(asctime)s.%(msecs)03d %(levelname)s: %(message)s',
    # datefmt='%Y-%m-%d %H:%M:%S',
    datefmt='%H:%M:%S',
)


def scNow():
    return int(round(time.time()))

def msNow():
    return int(round(time.time() * 1000))

def shellexec(strcmd):
    strout = ""

    try:
        logger.info("正在运行指令：" + strcmd)
        # print("正在运行指令：" + strcmd)
        process = subprocess.Popen(strcmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) #, cwd=os.environ['ZZSWMGR_WORK_DIR'], env={'ZZSWMGR_WORK_DIR':os.environ['ZZSWMGR_WORK_DIR']})
        # process.stdin.close()
        # process.stdout.close()
        process.wait()
        strout = process.stdout.read().decode("utf8")
        # print(strout)
        return process.returncode,strout
    except Exception as e:
        print("" + repr(e))
        return -1,strout

def shellexec_nowait(strcmd, workdir):
    try:
        logger.info("正在运行指令：" + strcmd)
        process = subprocess.Popen(strcmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=workdir) #, cwd=os.environ['ZZSWMGR_WORK_DIR'], env={'ZZSWMGR_WORK_DIR':os.environ['ZZSWMGR_WORK_DIR']})
        return 0,process
    except Exception as e:
        return -1,None




class TPutter(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        global qNotify
        while not exit_request:
            # print("put(1)")
            # self.t.reset_imgseq()
            # qNotify.put(1)
            qNotify.put(1)
            time.sleep(0.2)

class TGetScrImg(threading.Thread):
    def __init__(self, app, pipepath):
        threading.Thread.__init__(self)
        self.bimg = None
        self.imgseq = 999
        self.imgupd = msNow()
        self.dirtyNotifyPipe = pipepath
        self.app = app

    def GetImgViaUDSock(self):
        rlt = False
        try:
            self.bimg = None

            # unix domain sockets 连接写法
            server_address = '/exbin/ipc/virdisp.sock'
            socket_family = socket.AF_UNIX
            socket_type = socket.SOCK_STREAM

            # 其他代码完全一样
            sock = socket.socket(socket_family, socket_type)
            sock.connect(server_address)

            # print("self.imgseq: " + str(self.imgseq))
            bSeq = self.imgseq.to_bytes(length=4, byteorder='little', signed=False)
            sock.send(bSeq)

            magic = sock.recv(9)
            tmpseq= sock.recv(4)
            tmplen= sock.recv(4)
            # pkglen= int().from_bytes(tmplen, byteorder='big', signed=False)
            imgseq= int().from_bytes(tmpseq, byteorder='little', signed=False)
            pkglen= int().from_bytes(tmplen, byteorder='little', signed=False)
            # print("pkglen: " + str(pkglen))
            if pkglen > 0:
                self.bimg = bytes()
                while True:  
                    recv = sock.recv(1024*200)
                    if not recv:  
                        break
                    self.bimg = self.bimg + recv

                # print("self.imgseq: " + str(self.imgseq))
                self.imgseq = imgseq
                self.imgupd = msNow()

            # 关闭 UDS 连接
            sock.close()

            # 把 bytes 转成 sdl 格式的 image
            rlt = self.bytes2SDLImage()

        except Exception as e:
            rlt = False
            # print("图片接收出错")
            print(e)
        return rlt

    def bytes2SDLImage(self):
        global lkImage
        global bytesImg
        global pySDLImg

        rlt = False
        obj = None
        iimg = None
        if self.bimg != None and not CONV_IMG_IN_GUI_THREAD:
            # 互斥部分，获取图片数据
            try:
                obj = sdl2.SDL_RWFromMem(self.bimg, len(self.bimg))
                # print(obj)
                # iimg = sdl2.sdlimage.IMG_LoadTyped_RW(obj, 0, "JPG")
                iimg = sdl2.sdlimage.IMG_Load_RW(obj, 0)
                # print(iimg)
            except Exception as e:
                iimg = None
                print("bytes2SDLImage fail")
                print(e)
                self.Updating = False
                return False
            finally:
                if obj != None:
                    sdl2.SDL_RWclose(obj.contents)
        else:
            pass

        # print(self.bimg)
        lkImage.acquire()
        if CONV_IMG_IN_GUI_THREAD:
            bytesImg = self.bimg
            if self.bimg != None:
                rlt = True
        else:
            pySDLImg = iimg
            if iimg != None:
                rlt = True
        lkImage.release()
        return rlt

    def reset_imgseq(self):
        # print("pre self.imgseq: " + str(self.imgseq))
        self.imgseq = 0
        # print("aft self.imgseq: " + str(self.imgseq))

    # 屏幕有变化就通知的这个函数有点难搞，用管道画面会卡顿，用进程间信号又有信号丢失的情况
    def run(self):
        global app
        global eNotify
        global qNotify
        global exit_request
        pin = None
        print("屏幕传送线程 已启动")

        try:

            while not exit_request:

                # 信号量模式
                if DIRTYNOTIFY_VIA_SIGNAL:
                    data = qNotify.get()
                    # eNotify.wait()
                    # eNotify.clear()

                    # qsize = qNotify.qsize()
                    # print("qsize: " + str(qsize))
                    # if qsize > 60:
                    #     for i in range(30):
                    #         if exit_request:
                    #             break
                    #         else:
                    #             qNotify.get()

                    if exit_request:
                        break

                    # print("正在拉取图片")
                    rlt = self.GetImgViaUDSock()
                    # print("屏幕画面获取结果: " + str(rlt))
                    if not rlt:
                        # time.sleep(0.016)
                        pass
                    else:
                        if app != None:
                            event = sdl2.SDL_Event()
                            event.type = sdl2.SDL_USEREVENT + MSGID_UPDATEIMG
                            event.user = sdl2.SDL_UserEvent(type=event.type, timestamp=0, windowID=0, code=0)
                            sdl2.SDL_PushEvent(event)

                # 管道模式, 画面会卡顿
                else:
                    data=None
                    if pin == None:
                        pin = os.open(dirtyNotifyPipe, os.O_RDONLY|os.O_NONBLOCK )
                    try:
                        data = os.read(pin, 1)
                    except Exception as e:
                        # print("管道读取出错")
                        print(e)
                        time.sleep(133/1000)
                        continue
                    if data != None and len(data)>0 and app != None:
                        app.event_generate('<<SIG_SCREEN_DIRTY>>')
                    # else:
                    #     os.close(pin)
                    #     pin = None
                    #     time.sleep(33/1000)
                    # # time.sleep(1)

        except Exception as e:
            # print("loopReadDiryPipe 出错")
            print(e)
        finally:
            if pin != None:
                try:
                    os.close(pin)
                except Exception as ee:
                    pass

        print("屏幕传送线程 已退出")

class TinyScrcpyClient():
    # TinyScrcpyClient 类，参考自以下内容：
    # 1). https://github.com/razumeiko/py-android-viewer    # 旧，最简单，主要参考这个和 scrcpy 源码
    # 2). https://gitee.com/Haskely/py-scrcpy-client        # 旧
    # 3). https://github.com/leng-yue/py-scrcpy-client      # 新
    # 4). https://github.com/yixinNB/pyscrcpy               # 新，简单
    # 5). https://github.com/Genymobile/scrcpy              # 最新，scrcpy 客户端/服务端官方源码仓库

    video_socket = None
    control_socket = None
    resolution = None
    video_data_queue = Queue()

    # 操控相关的变量
    # 1). 操控相关的数据包类型
    SC_CONTROL_MSG_TYPE_INJECT_KEYCODE = 0
    SC_CONTROL_MSG_TYPE_INJECT_TEXT = 1
    SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT = 2
    SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT = 3
    SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON = 4
    SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL = 5
    SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL = 6
    SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS = 7
    SC_CONTROL_MSG_TYPE_GET_CLIPBOARD = 8
    SC_CONTROL_MSG_TYPE_SET_CLIPBOARD = 9
    SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE = 10
    SC_CONTROL_MSG_TYPE_ROTATE_DEVICE = 11

    # 2). 安卓的事件类型
    ACTION_DOWN = 0
    ACTION_UP   = 1
    ACTION_MOVE = 2
    move_step_length = 5  # Move by 5 pixels in one iteration
    move_steps_delay = 0.005  # Delay between each move step


    def __init__(
        self,
        serial: str = "",
        max_width: int = 0,
        bitrate: int = 8000000,
        max_fps: int = 30,
        adb_path: str = '/usr/local/bin/adb',
        ip: str = '127.0.0.1',
        port: int = 8081,
    ):
        """

        :param max_width: frame width that will be broadcast from android server
        :param bitrate:
        :param max_fps: 0 means not max fps.
        :param ip: android server IP
        :param adb_path: path to ADB
        :param port: android server port
        """
        self.adb_path = adb_path
        self.ip = ip
        self.port = port
        if serial != None and serial != "":
            self.arg_serial = "-s"
            self.str_serial = serial
        else:
            self.arg_serial = ""
            self.str_serial = ""

        # assert self.deploy_server(max_width, bitrate, max_fps)

        # self.codec = av.codec.CodecContext.create('h264', 'r')
        # self.connect_control_server()

    @staticmethod
    def adb_exec_server(str_displayid):
        global script_dir
        global process_control_server
        global scrcpy_client

        server_root = os.path.abspath(os.path.dirname(__file__))
        server_file_path = server_root + '/scrcpy-server-v2.5.jar'

        if not os.path.exists(server_file_path):
            logger.info('正在下载 scrcpy-server-v2.5')
            tmpurl = "${GITHUB_PROXY_HTTP}https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-server-v2.5"
            shrlt,strout = shellexec("wget {0} -O {1}".format(tmpurl, server_file_path))
            if shrlt != 0:
                App.showerr("错误", "无法将可执行文件传送到adb程序目录: /data/local/tmp/")
                return False

        logger.info('正在将可执行文件传送到adb程序目录: ' + script_dir)
        shrlt,strout = shellexec("adb -s {0} push {1}  /data/local/tmp/".format(adbconn, server_file_path))
        if shrlt != 0:
            App.showerr("错误", "无法将可执行文件传送到adb程序目录: /data/local/tmp/")
            return False

        logger.info('正在启动adb-socket转发进程')
        shrlt,strout = shellexec("adb -s {0} forward tcp:8081 localabstract:scrcpy".format(adbconn))
        if shrlt != 0:
            App.showerr("错误", "adb-socket转发进程启动失败")
            return False
        sleep(2)

        # github 主页上没有列出来的参数，可以参考以下两个文件：
        # 1) ./app/src/server.c                                         文件中的 execute_server(***) 
        # 2) ./server/src/main/java/com/genymobile/scrcpy/Options.java  文件中的 parse(***)

        # enum Level {
        #     VERBOSE, DEBUG, INFO, WARN, ERROR
        # }

        # adb -s 127.0.0.1:1111 shell CLASSPATH=/data/local/tmp/scrcpy-server2.jar app_process / com.genymobile.scrcpy.Server 1.25 log_level=info bit_rate=20000000 display_id=58

        logger.info('正在启动 scrcpy-server')
        shellexec("adb -s {0} shell kill `adb -s {1} shell pidof -s app_process` 2>/dev/null".format(adbconn, adbconn)) # 杀掉旧的scrcpy-server进程
        shrlt,process_control_server = shellexec_nowait(("adb -s {0} shell "
                                 "CLASSPATH=/data/local/tmp/scrcpy-server-v2.5.jar "
                                 "app_process / com.genymobile.scrcpy.Server "
                                 "2.5 log_level=VERBOSE display_id={1} "
                                 "control=\"true\" "
                                 "tunnel_forward=\"true\" "
                                 "video=\"false\" "
                                 "audio=\"false\" "
                                 "send_device_meta=\"false\" "
                                 "send_dummy_byte=\"true\"").format(adbconn, str_displayid),
                                workdir=server_root,
                                 )
        if shrlt != 0:
            App.showerr("错误", "scrcpy.Server 进程启动失败")
            return False
        
        logger.info("scrcpy-server启动成功，正在创建 Scrcpy Client 对象")
        scrcpy_client = TinyScrcpyClient(adb_path='/usr/bin/adb', serial=adbconn)
        scrcpy_client.connect_control_server()


        # 通知SDL2窗口线程
        if app != None:
            event = sdl2.SDL_Event()
            event.type = sdl2.SDL_USEREVENT + MSGID_SCSTARTED
            event.user = sdl2.SDL_UserEvent(type=event.type, timestamp=0, windowID=0, code=0)
            sdl2.SDL_PushEvent(event)

        return True

    def connect_control_server(self):
        global exit_request

        if exit_request:
            return

        # """
        # Connect to android server, there will be two sockets, video and control socket.
        # This method will set: video_socket, control_socket, resolution variables
        # """
        # try:
        #     logger.info('Connecting video socket')
        #     self.video_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        #     self.video_socket.connect((self.ip, self.port))
        # except Exception as e:
        #     print("" + repr(e))
        #     return

        # dummy_byte = self.video_socket.recv(1)
        # print(str(dummy_byte))
        # if not len(dummy_byte):
        #     raise ConnectionError('Did not receive Dummy Byte!')

        try:
            logger.info('正在建立操控通道，搞不懂scrcpy-server中的这一步为何会比较耗时')
            while not exit_request:
                self.control_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self.control_socket.connect((self.ip, self.port))

                dummy_byte = self.control_socket.recv(1)
                # print(str(dummy_byte))
                if not len(dummy_byte):
                    # raise ConnectionError('连接出错，首字节接收失败')
                    sleep(0.1)
                else:
                    return
        except Exception as e:
            print("" + repr(e))
            return

        # device_name = self.video_socket.recv(64).decode('utf-8')

        # if not len(device_name):
        #     raise ConnectionError('Did not receive Device Name!')
        # logger.info(f'Device Name: {device_name}')

        # res = self.video_socket.recv(4)
        # self.resolution = struct.unpack('>HH', res)
        # logger.info(f'Screen resolution: {self.resolution}')
        # self.video_socket.setblocking(False)

    # 操控相关的代码－－内部函数
    def _build_touch_message(self, x, y, evtype, action):
        res = None
        try:
            pointer_id = 0
            res = struct.pack(
                ">BBqiiHHHii",
                evtype,
                action,
                pointer_id,
                int(x),
                int(y),
                int(SCREEN_WIDTH),
                int(SCREEN_HEIGHT),
                int(0xFFFF), # pressure
                int(0),
                int(0),
            )
        except Exception as e:
            print("事件封包出错：\n" + repr(e))
        return res

    def _send_control_package(self, pkg):
        try:
            if pkg:
                if self.control_socket == None:
                    self.connect_control_server()
                self.control_socket.send(pkg)
            else:
                print("pkg为空")
        except Exception as e:
            print("事件传送失败:\n" + repr(e))
            if self.control_socket:
                self.control_socket.close()
                self.control_socket = None

    # 操控相关的代码
    def inject_touch_down(self, x, y):
        print("inject_touch_down")
        pkg = self._build_touch_message(x, y, self.SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, self.ACTION_DOWN)
        if pkg: self._send_control_package(pkg)

    def inject_touch_move(self, x, y):
        print("inject_touch_move")
        pkg = self._build_touch_message(x, y, self.SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, self.ACTION_MOVE)
        if pkg: self._send_control_package(pkg)

    def inject_touch_up(self, x, y):
        print("inject_touch_up")
        pkg = self._build_touch_message(x, y, self.SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, self.ACTION_UP)
        if pkg: self._send_control_package(pkg)

    def inject_text(self, str_text):
        print("inject_text")

        def _build_message(evtype, strbuffer):
            res = None
            if strbuffer == None:
                return res
        
            print(strbuffer)

            try:
                res = struct.pack(
                    ">Bi",
                    evtype,
                    len(strbuffer)
                ) + strbuffer
            except Exception as e:
                print("事件封包出错：\n" + repr(e))
            return res

        print(str_text)
        pkg = _build_message(self.SC_CONTROL_MSG_TYPE_INJECT_TEXT, str_text.encode("utf-8"))
        if pkg: self._send_control_package(pkg)

class MenuPopupWindow(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self, title="MenuPopup Demo")
        self.set_border_width(10)
        self.set_size_request(250, 200)
        vbox = Gtk.VBox(spacing=6)
        self.add(vbox)
        button = Gtk.Button("button")
        vbox.pack_start(button, False, False, 0)
        items = ["EditCopy", "测试", "EditSomething"]

        menu = Gtk.Menu.new()
        for item in items:
            item = Gtk.MenuItem(label=item)
            item.connect("activate", self.on_menu_selected)
            item.show()
            menu.append(item)
        button.connect("button_press_event", self.my_popup_handler, menu)

    @staticmethod
    def my_popup_handler(window, event, menu):
        if event.type == Gdk.EventType.BUTTON_PRESS:
            if event.button == Gdk.BUTTON_PRIMARY:
                menu.popup(None, None, None, None,
                           event.button, event.time)
                return True

        return False

    @staticmethod
    def on_menu_selected(widget, *args):
        print("Menu item " + widget.get_label() + " was selected")

class App():

    scrcpy = None
    window = None

    def __init__(self, hasui=True):
        self.vdisplayid=""

        if(hasui):
            # # 设置信号处理函数
            # signal.signal(SIG_SCREEN_DIRTY, self.sigHandler)

            # 创建界面窗口
            self.createui()

            # 把 pid 传给安卓端
            self.sendpid()

            self.startDefaultApp()

    def sdl_init():
        sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO)
        sdl2.sdlimage.IMG_Init(sdl2.sdlimage.IMG_INIT_JPG)

    def sigHandler(signum, frame):
        global app
        global eNotify
        global qNotify
        global exit_request

        # print('收到信号: ', signum)

        if exit_request:
            return

        if SIG_SCREEN_DIRTY == signum:
            event = sdl2.SDL_Event()
            event.type = sdl2.SDL_USEREVENT + MSGID_SCR_DIRTY
            event.user = sdl2.SDL_UserEvent(type=event.type, timestamp=0, windowID=0, code=0)
            sdl2.SDL_PushEvent(event)
        else:
            # print('Received signal: ', signum)
            pass

    def run_sdl2_in_thread():
        global app
        app = App()
        ret = app.run()
        return ret

    def run_gtk3_in_thread():
        win = MenuPopupWindow()
        win.connect("delete-event", Gtk.main_quit)
        win.show_all()
        Gtk.main()

    def get_osver():
        global OSVER
        shrlt,strout = shellexec("droidexec getprop ro.build.version.release")
        if shrlt == 0:
            OSVER=strout.replace('\n', '').replace('\r', '')
            print("OSVER: " + strout)

    def is_adb_installed():
        shrlt,strout = shellexec("which adb")
        return shrlt == 0

    def createui(self):
        self.window = sdl2.SDL_CreateWindow(title.encode(), sdl2.SDL_WINDOWPOS_CENTERED, sdl2.SDL_WINDOWPOS_CENTERED, SCREEN_WIDTH, SCREEN_HEIGHT, sdl2.SDL_WINDOW_HIDDEN)

        icon = "/exbin/tools/zzswmgr/appicons/android.jpg"
        image = sdl2.sdlimage.IMG_Load(icon.encode())
        sdl2.SDL_SetWindowIcon(self.window, image)
        sdl2.SDL_FreeSurface(image)

        self.ren = sdl2.SDL_CreateRenderer(self.window, -1, sdl2.SDL_RENDERER_ACCELERATED | sdl2.SDL_RENDERER_PRESENTVSYNC)

        self.tex = None
        bmp = sdl2.SDL_LoadBMP(b"/exbin/tools/zzswmgr/appicons/android.jpg")
        self.tex = sdl2.SDL_CreateTextureFromSurface(self.ren, bmp)
        sdl2.SDL_FreeSurface(bmp)

        sdl2.SDL_ShowWindow(self.window)

    def showerr(strtitle, strmessage):
        # Gtk.MessageType.ERROR
        # Gtk.MessageType.INFO  # https://qytz-notes.readthedocs.io/tech/PyGObject-Tutorial/dialogs.html
        dialog = Gtk.MessageDialog(
                transient_for=None,
                # # flags=Gtk.DialogFlags.MODAL,
                modal=True,
                message_type=Gtk.MessageType.ERROR,
                buttons=Gtk.ButtonsType.CANCEL,
                text=strtitle,
        )
        dialog.format_secondary_text(strmessage)
        btn = dialog.get_widget_for_response(Gtk.ResponseType.CANCEL)
        btn.set_label("好的")
        dialog.run()
        dialog.destroy()


        # global app
        # tmpwindow = None
        # print(app)
        # if app != None:
        #     tmpwindow = app.window
        # print(tmpwindow)

        # # 两个都可以，但中文信息乱码！
        # sdl2.messagebox.SDL_ShowSimpleMessageBox(sdl2.messagebox.SDL_MESSAGEBOX_ERROR, strtitle.encode('utf-8'), strmessage.encode('utf-8'), tmpwindow)
        # # sdl2.ext.show_alert(strtitle, strmessage, 'error', None) # tmpwindow) # 中文乱码！！！！

    def sendpid(self):
        global NOTIFY_PIPE
        self.t = None
        self.X_DAEMON_PIPE=os.getenv('X_DAEMON_PIPE')
        self.pid = os.getpid()
        self.dirtyNotifyPipe = "/exbin/tmp/virtDisplayNtPipe-{0}".format(self.pid)
        shellexec("rm -rf    /exbin/tmp/virtDisplayNtPipe-*")
        shellexec("mkfifo    {0}".format(self.dirtyNotifyPipe))
        shellexec("chmod 666 {0}".format(self.dirtyNotifyPipe))
        shellexec("echo \"#virtDisplaySetX11Pid {0} {1}\" > \"{2}\"".format(self.pid, SIG_SCREEN_DIRTY, NOTIFY_PIPE))

    def sigHandler_deprecated(self, signum, frame):
        global app
        global eNotify
        global qNotify
        global exit_request

        # print('收到信号: ', signum)

        if exit_request:
            return

        if SIG_SCREEN_DIRTY == signum:
            event = sdl2.SDL_Event()
            event.type = sdl2.SDL_USEREVENT + MSGID_SCR_DIRTY
            event.user = sdl2.SDL_UserEvent(type=event.type, timestamp=0, windowID=0, code=0)
            sdl2.SDL_PushEvent(event)
        else:
            # print('Received signal: ', signum)
            pass

    def run(self):
        global exit_request
        global app
        event = sdl2.SDL_Event()
        while sdl2.SDL_WaitEvent(ctypes.byref(event)) != 0:
            self.on_event_recv(event)
            if event.type == sdl2.SDL_QUIT:
                break

        sdl2.SDL_DestroyRenderer(self.ren)
        sdl2.SDL_DestroyWindow(self.window)
        sdl2.SDL_Quit()
        return 0

    def on_quit(self):
        global app
        global exit_request
        global process_control_server
        global NOTIFY_PIPE

        app = None
        exit_request=True


        # 退出 loopReadDiryPipe 线程
        # eNotify.set()
        if self.t != None:
            qNotify.put(1)

        print("请求退出")

        self.stop_pre_app()

        if process_control_server:
            print("正在结束 scrcpy-server")
            process_control_server.kill() # terminate()
        shellexec("adb -s {0} shell kill `adb -s {1} shell pidof -s app_process` 2>/dev/null".format(adbconn, adbconn)) # 杀掉旧的scrcpy-server进程

        # 等待子线程完全退出
        if self.t != None:
            self.t.join

        # 销毁安卓虚拟桌面
        shellexec("echo \"#deleteAndroidVirtDisplay\" > \"{0}\"".format(NOTIFY_PIPE))

    # 事件处理
    def on_event_recv(self, event):
        global exit_request

        # # 参考：cat /usr/lib/python3/dist-packages/sdl2/events.py
        # print("event.type: " + str(event.type))

        if event.type == sdl2.SDL_QUIT:
            self.on_quit()
            return
        if event.type == sdl2.SDL_TEXTINPUT:
            if self.scrcpy:
                try:
                    utf8str=event.text.text.decode()
                    self.scrcpy.inject_text(utf8str)
                except Exception as e:
                    print(e)
            return
        if event.type == sdl2.SDL_USEREVENT + MSGID_SCR_DIRTY:
            qsize = qNotify.qsize()
            # print("qsize: " + str(qsize))
            if qsize > 100:
                return

            if qsize < 10:
                for i in range(50):
                    if exit_request:
                        return
                    else:
                        qNotify.put(i)
            else:
                qNotify.put(1)

        if event.type == sdl2.SDL_USEREVENT + MSGID_UPDATEIMG:
            self.updateimg()
            # pass

        if event.type == sdl2.SDL_MOUSEBUTTONDOWN:
            tmpevt = event.button
            if (tmpevt.button == sdl2.SDL_BUTTON_LEFT):
                if self.scrcpy:
                    try:
                        self.scrcpy.inject_touch_down(tmpevt.x, tmpevt.y)
                    except Exception as e:
                        print(e)
            elif (tmpevt.button == sdl2.SDL_BUTTON_RIGHT):
                pass

        if event.type == sdl2.SDL_MOUSEMOTION:
            tmpevt = event.button
            if (tmpevt.button == sdl2.SDL_BUTTON_LEFT):
                if self.scrcpy:
                    try:
                        self.scrcpy.inject_touch_move(tmpevt.x, tmpevt.y)
                    except Exception as e:
                        print(e)

        if event.type == sdl2.SDL_MOUSEBUTTONUP:
            tmpevt = event.button
            if (tmpevt.button == sdl2.SDL_BUTTON_LEFT):
                if self.scrcpy:
                    try:
                        self.scrcpy.inject_touch_up(tmpevt.x, tmpevt.y)
                    except Exception as e:
                        print(e)
            elif (tmpevt.button == sdl2.SDL_BUTTON_RIGHT):
                # self.t.reset_imgseq()
                # qNotify.put(1)
                self.updateimg()

        if event.type == sdl2.SDL_USEREVENT + MSGID_SCSTARTED:
            self.new_sc_client()

    def updateimg(self):
        global bytesImg
        global pySDLImg
        global nFrameCache
        global secStart

        # print("CONV_IMG_IN_GUI_THREAD: " + str(CONV_IMG_IN_GUI_THREAD))

        # 计算fps
        if secStart < 1:
            secStart = scNow()

        secNow = scNow()
        secDlt = secNow - secStart
        fps = 0
        if secDlt > 0:
            fps = int(round(nFrameCache/secDlt))
        # print("fps: " + str(fps))

        if CONV_IMG_IN_GUI_THREAD:
            obj = None
            iimg = None
            # 互斥部分，获取图片数据
            lkImage.acquire()
            try:
                if bytesImg != None:
                    obj = sdl2.SDL_RWFromMem(bytesImg, len(bytesImg))
                    # print(obj)
                    # iimg = sdl2.sdlimage.IMG_LoadTyped_RW(obj, 0, "JPG")
                    iimg = sdl2.sdlimage.IMG_Load_RW(obj, 0)
            except Exception as e:
                iimg = None
                print(e)
                self.Updating = False
                return
            finally:
                lkImage.release()
                if obj != None:
                    sdl2.SDL_RWclose(obj.contents)

            if iimg != None:
                nFrameCache+=1
                try:
                    self.tex = sdl2.SDL_CreateTextureFromSurface(self.ren, iimg)
                    sdl2.SDL_RenderClear(self.ren)
                    sdl2.SDL_RenderCopy(self.ren, self.tex, None, None)
                    sdl2.SDL_RenderPresent(self.ren)
                    # sdl2.SDL_Delay(100)
                except Exception as ee:
                    print(repr(ee))
                finally:
                    if iimg != None:
                        sdl2.SDL_FreeSurface(iimg)
        else:
            lkImage.acquire()
            try:
                    # sdl2.SDL_Delay(100)
                if pySDLImg != None:
                    nFrameCache+=1
                    self.tex = sdl2.SDL_CreateTextureFromSurface(self.ren, pySDLImg)
                    sdl2.SDL_RenderClear(self.ren)
                    sdl2.SDL_RenderCopy(self.ren, self.tex, None, None)
                    sdl2.SDL_FreeSurface(pySDLImg)

                # 绘制
                sdl2.SDL_RenderPresent(self.ren)
            except Exception as ee:
                print(repr(ee))
            finally:
                lkImage.release()

    def popupmenu(self):
        items = ["EditCopy", "测试", "EditSomething"]
        menu = Gtk.Menu.new()
        for item in items:
            item = Gtk.MenuItem(label=item)
            # item.connect("activate", self.on_menu_selected)
            item.show()
            menu.append(item)
        menu.popup(None, None, None, None, 0, scNow())

    def createAndroidVirDisplay():
        global NOTIFY_PIPE

        try:

            shrlt,strout = shellexec("adb -s " + adbconn + " shell dumpsys display|grep \"type=EXTERNAL\"")
            # print("shrlt: " + str(shrlt))
            if shrlt == 0:
                return False

            # dpi 调整后，需要重启虚拟电脑
            tmpDPI=144
            tmpDPI=-1
            shrlt,strout = shellexec("echo \"#createAndroidVirtDisplay {0} {1} {2}\" > \"{3}\"".format(SCREEN_WIDTH, SCREEN_HEIGHT, tmpDPI, NOTIFY_PIPE))
            if shrlt != 0:
                return False
            return True
        except Exception as e:
            print(e)
            return False
    def getAndroidVirDisplayId():
        try:
            shrlt,strout=shellexec("adb -s " + adbconn + " shell dumpsys window displays|grep mDisplayId|grep -v mDisplayId=0|grep -v \"organized\"|head -n 1|awk '{ printf($2);  }'|cut -c 12-")
            if shrlt != 0:
                return False,""
            tmp_displayid=strout.replace('\n', '').replace('\r', '')
            if tmp_displayid == "":
                return False,""
            return True,tmp_displayid
        except Exception as e:
            print(e)
            return False
    def delay_start_app(self, strpkg, stract):
        self.after_idle(self.startapp, strpkg, stract)
        # self.startapp(strpkg, stract)
    def stop_pre_app(self):
        strRecentPkg=None
        shrlt,strout = shellexec("cat /tmp/strRecentPkg.txt 2>/dev/null")
        if shrlt == 0:
            strRecentPkg=strout.replace('\n', '').replace('\r', '')

        if strRecentPkg != None and strRecentPkg != "" and strRecentPkg != "com.zzvm":
            print("正在杀掉应用：" + strRecentPkg)
            shrlt,strout = shellexec("adb -s " + adbconn + " shell am force-stop " + strRecentPkg)
        return strRecentPkg
    def startapp(self, strpkg, stract):
        strapp = strpkg + "/" + stract
        try:
            opt_kill_exist_process="-S"
            shrlt,strout = shellexec("adb connect 127.0.0.1:" + str(adbport))
            if shrlt != 0:
                App.showerr("错误", "adb连接失败\n请先运行桌面上的adbme，并按步骤连接设备")
                return False
            
            virrlt = App.createAndroidVirDisplay()
            if not virrlt:
                App.showerr("错误", "无法创建安卓虚拟桌面")
                return False
            
            virrlt,self.vdisplayid = App.getAndroidVirDisplayId()
            if not virrlt:
                App.showerr("错误", "无法获取安卓虚拟桌面id")
                return False

            # 先结束上次启动的应用，不然开的进程太多，容易导致安卓杀进程
            strRecentPkg = self.stop_pre_app()

            if strRecentPkg == "com.zzvm":
                opt_kill_exist_process = ""

            # shrlt,strout = shellexec("/exbin/tools/vm_createAndroidVirtDisplay2.sh " + strapp)
            shrlt,strout = shellexec("adb -s {0} shell am start --display {1} {2} {3}".format(adbconn, self.vdisplayid, opt_kill_exist_process, strapp))
            if shrlt != 0:
                App.showerr("错误", strpkg + "\napp启动失败，请重试")
                return False
            else:
                shellexec("echo \"{0}\">/tmp/strRecentPkg.txt".format(strpkg))
                # for i in range(50):
                #     qNotify.put(i)

            return True
        except Exception as e:
            App.showerr("错误", strpkg + "\napp启动失败，请重试")
            print(e)
            return False
    def startDefaultApp(self):
        global qNotify

        shrlt,strout = shellexec("echo \"adbsrv\" > \"{0}\"".format(self.X_DAEMON_PIPE))
        time.sleep(1)

        strRecentPkg = DEF_PKG
        if self.startapp(strRecentPkg, DEF_ACT):
            self.rendering=True

            # 启动 "启动scrcpy-server" 的线程，比较耗时， 所以开个线程来做这个动作
            tmp_thread = threading.Thread(target=TinyScrcpyClient.adb_exec_server, args=(self.vdisplayid,))
            tmp_thread.start()

            # 启动屏幕获取线程
            self.t=TGetScrImg(self, self.dirtyNotifyPipe)
            self.t.start()

            # 启动Putter线程
            self.p=TPutter()
            self.p.start()

    def new_sc_client(self):
        global scrcpy_client
        try:
            if scrcpy_client != None and scrcpy_client.control_socket != None:
                self.scrcpy = scrcpy_client
                logger.info("操控通道已连通")


        except Exception as e:
            self.scrcpy = None
            print("" + repr(e))
            return






if __name__ == "__main__":
    # 获取脚本所在目录的路径
    script_nam = os.path.basename(__file__)
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # 更改工作目录为脚本所在目录
    os.chdir(script_dir)

    App.sdl_init()

    App.get_osver()
    if( int(OSVER) < 10 ):
        App.showerr("错误", "此功能仅可在安卓10以上使用")
        exit(-1)

    if not App.is_adb_installed():
        App.showerr("错误", "请先在 软件管家 中安装adb")
        exit(-1)

    argc=len(sys.argv)
    if argc > 1:
        strapp = sys.argv[1]
        strlist = strapp.split("/")
        if(len(strlist) >= 2):
            DEF_PKG=strlist[0]
            DEF_ACT=strlist[1]

    self_pid = os.getpid()
    shrlt,strout = shellexec("ps ax|grep -v grep|grep -v {0}|grep {1}".format(self_pid, script_nam))
    if shrlt == 0:
        # print("已在运行")
        # App.showerr("错误", "已在运行")
        virrlt,tmp_displayid = App.getAndroidVirDisplayId()
        if not virrlt:
            App.showerr("错误", "无法获取安卓虚拟桌面id")
            exit(-1)

        shrlt,strout = shellexec("cat /tmp/strRecentPkg.txt 2>/dev/null")
        if shrlt == 0:
            tmppkg=strout.replace('\n', '').replace('\r', '')
            print("正在杀掉应用：" + tmppkg)
            shrlt,strout = shellexec("adb -s " + adbconn + " shell am force-stop " + tmppkg)

        strpkg = DEF_PKG
        stract = DEF_ACT
        strapp = strpkg + "/" + stract
        shrlt,strout = shellexec("adb -s {0} shell am start --display {1} -S {2}".format(adbconn, tmp_displayid, strapp))
        if shrlt != 0:
            App.showerr("错误", strpkg + "\napp启动失败，请重试")
        else:
            shellexec("echo \"{0}\">/tmp/strRecentPkg.txt".format(strpkg))
    else:

        # 设置信号处理函数
        signal.signal(SIG_SCREEN_DIRTY, App.sigHandler)

        # main-thread
        mt = threading.Thread(target=App.run_sdl2_in_thread)
        mt.start()

        # mp = threading.Thread(target=App.run_gtk3_in_thread)
        # mp.start()

        mt.join()
        sys.exit(0)
